/*
 * Copyright (C) 2012 Damien Girard <dgirard@nativesoft.fr>
 * Copyright (C) 2012 Sebastien Baudrand <sebbaudrand@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package fr.umlv.qroxy.communication;

import fr.umlv.qroxy.cache.CacheRequest;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Qroxy single thread server.
 *
 * @author Damien Girard <dgirard@nativesoft.fr>
 * @author Sebastien Baudrand <sebbaudrand@gmail.com>
 */
public class Server { //implements Runnable {

    private static final Logger logger = Logger.getLogger(Server.class.getName());
    private final int port;
    private final String cacheDirectory;

    /**
     * Qroxy proxy server.
     *
     * @param port Port to bind to.
     */
    public Server(int port, String cacheDirectory) {
        this.port = port;
        this.cacheDirectory = cacheDirectory;
    }

    //@Override
    public void run() throws IOException, Exception {
        ServerSocketChannel serverChannel = null;
        //try {
        serverChannel = ServerSocketChannel.open();

        // Non blocking.
        serverChannel.configureBlocking(false);

        // Binding.
        serverChannel.socket().bind(new InetSocketAddress(port));

        // Select
        Selector selector = Selector.open();
        serverChannel.register(selector, serverChannel.validOps());

        // Timeout checking task.
        //long timeoutTimer = System.currentTimeMillis() + 1000;

        while (!Thread.interrupted()) {
            int numberOfSelected = selector.select(250);

            //if (timeoutTimer < System.currentTimeMillis()) { // Checking timeouts and checking cacheTransfer.
            { // Checking timeout and unlocking QoS operations.
                long currentTime = System.currentTimeMillis();

                Set keys = selector.keys();
                Iterator itr = keys.iterator();
                while (itr.hasNext()) {
                    SelectionKey key = (SelectionKey) itr.next();
                    if (!key.isValid()) {
                        continue;
                    }

                    if (key.attachment() instanceof Connection) {
                        Connection connection = (Connection) key.attachment();
                        if (connection.getTimeoutTime() != null) {
                            if (connection.getTimeoutTime() < currentTime) {
                                System.out.println("Timeout. Closing");
                                connection.close();
                                //key.cancel();
                            } else {
                                connection.unlockQOS(); // Unlocking waiting QoS operations.
                            }
                        }
                    } else if (key.attachment() instanceof CacheRequest) {
                        if (((CacheRequest) key.attachment()).isTimeout()) {
                            logger.fine("Closing CacheRequest because of timeout.");
                            ((CacheRequest) key.attachment()).close();
                            //key.cancel();
                        }
                    }
                }
            }
            // Somes connection may have been closed, relaunching the select.
            //timeoutTimer = System.currentTimeMillis() + 1000; // In 500ms new timeout check.
            //}

            // Anything selected?
            if (numberOfSelected <= 0) {
                continue;
            }

            Set selected = selector.selectedKeys();
            Iterator itr = selected.iterator();
            while (itr.hasNext()) {
                SelectionKey key = (SelectionKey) itr.next();
                itr.remove();

                // Sanity check.
                if (!key.isValid()) {
                    continue;
                }

                if (key.isValid() && key.isAcceptable()) {
                    // If an object is registered with this key,
                    // I call the handlerAccept of the registered key. Otherwise
                    // I create a new connection object.

                    if (key.attachment() != null) {
                        ((Communicable) key.attachment()).handleAccept(key.channel(), selector);
                    } else {
                        SocketChannel newChannel = ((ServerSocketChannel) key.channel()).accept();
                        newChannel.socket().setSoLinger(true, 1000);
                        newChannel.configureBlocking(false);
                        SelectionKey newSelectionKey = newChannel.register(selector, SelectionKey.OP_READ);
                        Connection newConnection = new Connection(newChannel, newSelectionKey, cacheDirectory);
                        newSelectionKey.attach(newConnection);
                        logger.log(Level.FINE, "Accepted new client: " + newChannel.socket().getRemoteSocketAddress());
                    }
                }
                if (key.isValid() && key.isConnectable()) {
                    ((Communicable) key.attachment()).handleConnect(key.channel(), key);
                }
                if (key.isValid() && key.isWritable()) {
                    ((Communicable) key.attachment()).handleWrite(key.channel(), key);
                }
                if (key.isValid() && key.isReadable()) {
                    ((Communicable) key.attachment()).handleRead(key.channel(), key);
                }
            }
        }
    }
}
